﻿using System.Xml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.IO;
using Common.Logging;

namespace Util.Props
{
    /// <summary>
    /// A SAX XML Handler implementation that builds up the map of raw property data objects
    /// </summary>
    public class ConfigHandler : Object
    {
        protected RawPropertyData rpd=null;
        protected ILocator locator;
        protected List<String> itemList;
        protected String itemListName;
        protected StringBuilder curItem;

        protected  Dictionary<String, RawPropertyData> rpdMap=null;
        protected  Dictionary<String, String> globalProperties=null;

        private Boolean replaceDuplicates=false;
        private String baseURL=String.Empty;

        public ConfigHandler(Dictionary<String, RawPropertyData> rpdMap, Dictionary<String, String> globalProperties,
                        Boolean replaceDuplicates, String baseURL)
        {
            this.rpdMap = rpdMap;
            this.globalProperties = globalProperties;
            this.replaceDuplicates = replaceDuplicates;
            this.baseURL = baseURL;
        }

        public ConfigHandler(Dictionary<String, RawPropertyData> rpdMap, Dictionary<String, String> globalProperties)
            : this(rpdMap, globalProperties, false, null)
        {
            
        }

        /// <summary>
        /// Receive notification of the start of an element.
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="localName"></param>
        /// <param name="qName"></param>
        /// <param name="attributes"></param>
        public void startElement(String uri, String localName, String qName, Dictionary<String,String> attributes)
        {
            if (qName.Equals("config")) 
            {
                // test if this configuration extends another one
                if (attributes.ContainsKey("extends"))
                {
                    String extendedConfigName = attributes["extends"];
                    if (!String.IsNullOrEmpty(extendedConfigName))
                    {
                        mergeConfigs(extendedConfigName, true);
                        replaceDuplicates = true;
                    }
                }
            } 
            else if (qName.Equals("include")) 
            {
                if (attributes.ContainsKey("file"))
                {
                    mergeConfigs(attributes["file"], false);
                }
            } 
            else if (qName.Equals("extendwith")) 
            {
                if (attributes.ContainsKey("file"))
                {
                    mergeConfigs(attributes["file"], true);
                }
            } 
            else if (qName.Equals("component")) 
            {
                if (!attributes.ContainsKey("name") || !attributes.ContainsKey("type"))
                {
                    throw new Exception("component element must have 'name' and 'type' attributes"/*, locator*/);
                }
                String curComponent = attributes["name"].ToLower();
                String curType = attributes["type"];
                if (rpdMap!=null && rpdMap.ContainsKey(curComponent) && !replaceDuplicates) 
                {
                    throw new Exception("duplicate definition for " + curComponent/*, locator*/);
                }
                rpd = new RawPropertyData(curComponent, curType);
            } 
            else if (qName.Equals("property")) 
            {
                if (attributes.Count != 2 || !attributes.ContainsKey("name") || !attributes.ContainsKey("value")) 
                {
                    throw new Exception("property element must only have 'name' and 'value' attributes"/*, locator*/);
                }
                String name = attributes["name"];
                String value = attributes["value"];

                if (rpd == null) 
                {
                    // we are not in a component so add this to the global
                    // set of symbols
    //                    String symbolName = "${" + name + "}"; // why should we warp the global props here
                    globalProperties.Add(name, value);
                } 
                else if (rpd.contains(name) && !replaceDuplicates) 
                {
                    throw new Exception("Duplicate property: " + name/*, locator*/);
                } 
                else 
                {
                    rpd.add(name, value);
                }
            } 
            else if (qName.Equals("propertylist")) 
            {

                if (attributes.Count != 1 || !attributes.ContainsKey("name")) 
                {
                    throw new Exception("list element must only have the 'name'  attribute"/*, locator*/);
                }
                itemListName = attributes["name"];
                itemList = new List<String>();
            } 
            else if (qName.Equals("item")) 
            {
                if (attributes.Count != 0) 
                {
                    throw new Exception("unknown 'item' attribute"/*, locator*/);
                }
                curItem = new StringBuilder();
            } 
            else 
            {
                throw new Exception("Unknown element '" + qName + '\''/*, locator*/);
            }
        }

        /// <summary>
        /// Receive notification of character data inside an element.
        /// By default, do nothing. Application writers may override this method to take specific actions for each chunk of character data (such as adding the data to a node or buffer, or printing it to a file).
        /// </summary>
        /// <param name="ch"></param>
        /// <param name="start"></param>
        /// <param name="length"></param>
        public void characters(char[] ch, int start, int length)
        {
            if (curItem != null) 
            {
                curItem.Append(ch, start, length);
            }
        }

        /// <summary>
        ///  Receive notification of the end of an element.
        ///  By default, do nothing. Application writers may override this method in a subclass to take specific actions at the end of each element (such as finalising a tree node or writing output to a file).
        /// </summary>
        /// <param name="uri"></param>
        /// <param name="localName"></param>
        /// <param name="qName"></param>
        public void endElement(String uri, String localName, String qName)
        {
            if (qName.Equals("component")) 
            {
                rpdMap.Add(rpd.name, rpd);
                rpd = null;
            } 
            else if (qName.Equals("property")) 
            {
                // nothing to do
            } 
            else if (qName.Equals("propertylist")) 
            {
                if (rpd.contains(itemListName)) 
                {
                    throw new Exception("Duplicate property: " + itemListName/*, locator*/);
                } 
                else {
                    rpd.add(itemListName, itemList);
                    itemList = null;
                }
            } 
            else if (qName.Equals("item")) 
            {
                itemList.Add(curItem.ToString().Trim());
                curItem = null;
            }
        }

        /// <summary>
        /// Receive a Locator object for document events. 
        /// </summary>
        /// <param name="locator"></param>
        public void setDocumentLocator(ILocator locator) 
        {
            this.locator = locator;
        }

        private void mergeConfigs(String configFileName, Boolean replaceDuplicates) 
        {
            try 
            {
                String fileURL = baseURL + configFileName;

                ILog logger = LogManager.GetLogger(typeof(ConfigHandler).Name);
                logger.Trace((replaceDuplicates ? "extending" : "including") + " config:" + fileURL.ToString());

                SaxLoader saxLoader = new SaxLoader(fileURL, globalProperties, rpdMap, replaceDuplicates);
                saxLoader.load();
            } 
            catch (IOException e) 
            {
                throw new SystemException("Error while processing <include file=\"" + configFileName + "\">: " + e, e);
            } 
            catch (UriFormatException e) 
            {
                throw e;
            }
        }

    }
}
